<!DOCTYPE html>
<meta charset="utf-8">
<title>Tests for PaymentRequest interface</title>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script>
function substitute(originalObject, substituteKeyValuePairs) {
    for (var key in originalObject) {
        if (originalObject.hasOwnProperty(key) && substituteKeyValuePairs.hasOwnProperty(key)) {
            originalObject[key] = substituteKeyValuePairs[key];
        }
    }
}

function buildItem(optionalSubstituteKeyValuePairs) {
    var item = {
        'id': 'item_id',
        'label': 'Item Description',
        'amount': {
            'currency': 'USD',
            'value': '10.00'
        },
        'selected': false
    };

    if (optionalSubstituteKeyValuePairs) {
        for (var key in optionalSubstituteKeyValuePairs) {
            assert_true(item.hasOwnProperty(key) || item['amount'].hasOwnProperty(key), 'Unrecognized substitution key "' + key + '"');
        }

        substitute(item, optionalSubstituteKeyValuePairs);
        substitute(item['amount'], optionalSubstituteKeyValuePairs);
    }

    return item;
}

function setValue(obj, key, val) {
    keys = key.split(/\./);
    key = keys.pop();
    keys.forEach((k) => { obj = obj[k]; });
    assert_true(obj != undefined);
    obj[key] = val;
}

function buildDetails(optionalDetailName, optionalSubstituteKeyValuePairs) {
    var details = {
        'total': buildItem(),
        'displayItems': [buildItem()],
        'shippingOptions': [buildItem()],
        'modifiers': [{
            'supportedMethods': 'foo',
            'total': buildItem(),
            'additionalDisplayItems': [buildItem()]
        }]
    };

    if (optionalDetailName)
        setValue(details, optionalDetailName, buildItem(optionalSubstituteKeyValuePairs));

    return details;
}

test(function() {
    new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails(), {});
}, 'Creating a PaymentRequest with empty parameters should not throw or crash.');

test(function() {
    new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails(), {}, '');
}, 'Creating a PaymentRequest with extra parameters should not throw or crash.');

test(function() {
    new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails());
}, 'Creating a PaymentRequest with omitted optional parameters should not throw or crash.');

test(function() {
    new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails(), undefined);
}, 'Creating a PaymentRequest with undefined optional parameters should not throw or crash.');

test(function() {
    new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails(), null);
}, 'Creating a PaymentRequest with null optional parameters should not throw or crash.');

test(function() {
    var request = new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails());
    assert_readonly(request, 'shippingAddress', 'PaymentRequest should have a readonly shippingAddress property.');
    assert_readonly(request, 'shippingOption', 'PaymentRequest should have a readonly shippingOption property.');
    assert_readonly(request, 'shippingType', 'PaymentRequest should have a readonly shippingType property.');
}, 'PaymentRequest should have readonly shippingAddress and shippingOption properties.');

test(function() {
    var request = new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails());
    assert_not_equals(request.onshippingaddresschange, undefined, 'PaymentRequest should have onShippingAddressChange event.');
    assert_not_equals(request.onshippingoptionchange, undefined, 'PaymentRequest should have onShippingOptionChange event.');
}, 'PaymentRequest should have onShippingAddressChange and onShippingOptionChange events.');

test(function() {
    var request = new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails());
    assert_not_equals(request.abort, undefined, 'PaymentRequest should have abort() method.');
    assert_not_equals(request.show, undefined, 'PaymentRequest should have show() method.');
}, 'PaymentRequest should have methods abort() and show().');

test(function() {
    var request = new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails());
    request.show();
    request.abort();
}, 'PaymentRequest.abort() and PaymentRequest.show() should take no parameters.');

test(function() {
    var request = new PaymentRequest([{'supportedMethods': 'foo', 'data': {'foo': {'gateway': 'bar'}}}], buildDetails(), {'requestShipping': true});
    request.show();
    request.abort();
}, 'Valid data causes no errors.');

test(function() {
    var request = new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails('shippingOptions.0', {'id': 'standard'}));
    assert_equals(null, request.shippingOption);
}, 'Shipping option identifier should be null if shipping request is omitted.');

test(function() {
    var request = new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails('shippingOptions.0', {'id': 'standard'}), {'requestShipping': false});
    assert_equals(null, request.shippingOption);
}, 'Shipping option identifier should be null if shipping is explicitly not requested.');

test(function() {
    var request = new PaymentRequest([{'supportedMethods': 'foo'}], {'total': buildItem(), 'displayItems': [buildItem()]}, {'requestShipping': true});
    assert_equals(null, request.shippingOption);
}, 'Shipping option identifier should be null if no shipping options are provided.');

test(function() {
    var request = new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails('shippingOptions.0', {'selected': false}), {'requestShipping': true});
    assert_equals(null, request.shippingOption);
}, 'Shipping option identifier should be null if the single provided option is not selected.');

test(function() {
    var request = new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails('shippingOptions.0', {'id': 'standard', 'selected': true}), {'requestShipping': true});
    assert_equals('standard', request.shippingOption);
}, 'Shipping option identifier should default to the single provided option if it is selected.');

test(function() {
    var shippingOptions = [buildItem({'id': 'standard'}), buildItem({'id': 'express'})];
    var request = new PaymentRequest([{'supportedMethods': 'foo'}], {'total': buildItem(), 'displayItems': [buildItem()], 'shippingOptions': shippingOptions}, {'requestShipping': true});
    assert_equals(null, request.shippingOption);
}, 'Shipping option identifier should be null if multiple unselected shipping options are provided.');

test(function() {
    var shippingOptions = [buildItem({'id': 'standard', 'selected': true}), buildItem({'id': 'express'})];
    var request = new PaymentRequest([{'supportedMethods': 'foo'}], {'total': buildItem(), 'displayItems': [buildItem()], 'shippingOptions': shippingOptions}, {'requestShipping': true});
    assert_equals('standard', request.shippingOption);
}, 'Shipping option identifier should default to the selected shipping option.');

test(function() {
    var shippingOptions = [buildItem({'id': 'standard', 'selected': true}), buildItem({'id': 'express', 'selected': true})];
    var request = new PaymentRequest([{'supportedMethods': 'foo'}], {'total': buildItem(), 'displayItems': [buildItem()], 'shippingOptions': shippingOptions}, {'requestShipping': true});
    assert_equals('express', request.shippingOption);
}, 'Shipping option identifier should default to the last selected shipping option, if multiple are selected.');

test(function() {
    var shippingOptions = [buildItem({'id': 'express', 'selected': false}), buildItem({'id': 'express', 'selected': true})];
    assert_throws_js(
    TypeError,
    () => {
      new PaymentRequest([{'supportedMethods': 'foo'}], {'total': buildItem(), 'displayItems': [buildItem()], 'shippingOptions': shippingOptions}, {'requestShipping': true});
    },
    "Expected to throw a TypeError because duplicate shipping option IDs"
  );
}, 'A TypeError should be thrown for duplicate shipping option identifiers.');

test(function() {
    var request = new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails(), {'requestShipping': false});
    assert_equals(null, request.shippingType);
}, 'Shipping type should be null if shipping is explicitly not requested.');

test(function() {
    var request = new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails(), {'requestShipping': true});
    assert_equals('shipping', request.shippingType);
}, 'Shipping type should be \'shipping\' by default if shipping type isn\'t specified.');

test(function() {
    var request = new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails(), {'requestShipping': false, 'shippingType': 'shipping'});
    assert_equals(null, request.shippingType);
}, 'Shipping type should be null if shipping type is specified but requestShipping is false.');

test(function() {
    var request = new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails(), {'requestShipping': true, 'shippingType': 'shipping'});
    assert_equals('shipping', request.shippingType);
}, 'Shipping type should be \'shipping\' if shipping type is specified as \'shipping\'.');

test(function() {
    var request = new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails(), {'requestShipping': true, 'shippingType': 'delivery'});
    assert_equals('delivery', request.shippingType);
}, 'Shipping type should be \'delivery\' if shipping type is specified as \'delivery\'.');

test(function() {
    var request = new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails(), {'requestShipping': true, 'shippingType': 'pickup'});
    assert_equals('pickup', request.shippingType);
}, 'Shipping type should be \'pickup\' if shipping type is specified as \'pickup\'.');

test(function() {
    var request = new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails(), {'requestShipping': true, 'shippingType': undefined});
    assert_equals('shipping', request.shippingType);
}, 'Shipping type should be \'shipping\' if shipping type is specified as undefined.');

test(function() {
    new PaymentRequest([{'supportedMethods': 'foo'}], {'total': buildItem(), 'displayItems': undefined});
}, 'Undefined display items should not throw.');

test(function() {
    new PaymentRequest([{'supportedMethods': 'foo'}], {'total': buildItem(), 'displayItems': []});
}, 'Empty display items should not throw.');

test(function() {
    new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails('total', {'value': '0'}));
}, 'Non-negative total value should not throw.');

test(function() {
    new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails('displayItems.0', {'value': '-0.01'}));
}, 'Negative line item value should not throw.');

test(function() {
    new PaymentRequest([{'supportedMethods': 'foo'}], {'total': buildItem(), 'modifiers': undefined});
}, 'Undefined modifiers should not throw.');

test(function() {
    new PaymentRequest([{'supportedMethods': 'foo'}], {'total': buildItem(), 'modifiers': [{'supportedMethods': 'foo', 'total': buildItem({'value': '0.0'})}]});
}, 'Non-negative total value in PaymentDetailsModifier should not throw.');

test(function() {
    new PaymentRequest([{'supportedMethods': 'foo'}], {'total': buildItem(), 'modifiers': [{'supportedMethods': 'foo'}, {'supportedMethods': 'foo'}]});
}, 'Duplicate supported payment method identifiers in separate methoData objects of modifiers should not throw.');

test(function() {
    new PaymentRequest([{'supportedMethods': 'https://android.com/pay', 'data': {'environment': 'TEST', 'merchantName': 'Merchant Inc', 'merchantId': '123', 'allowedCardNetworks': ['AMEX', 'DISCOVER', 'MASTERCARD', 'VISA'], 'paymentMethodTokenizationParameters': {'tokenizationType': 'GATEWAY_TOKEN', 'parameters': {'key': 'value'}}}}], buildDetails());
}, 'Android Pay parameters for test environment with gateway token should not throw.');

test(function() {
    new PaymentRequest([{'supportedMethods': 'https://android.com/pay', 'data': {'environment': 'PRODUCTION', 'merchantName': 'Merchant Inc', 'merchantId': '123', 'allowedCardNetworks': ['AMEX', 'DISCOVER', 'MASTERCARD', 'VISA'], 'paymentMethodTokenizationParameters': {'tokenizationType': 'NETWORK_TOKEN', 'parameters': {'key': 'value'}}}}], buildDetails());
}, 'Android Pay parameters for produciton environment with network token should not throw.');

test(function() {
  new PaymentRequest([{'supportedMethods': 'basic-card', 'data': {'supportedTypes': ['debit'], 'supportedNetworks': ['visa']}}], buildDetails());
}, 'Basic card parameters should not throw.');

test(function() {
  new PaymentRequest([{'supportedMethods': 'basic-card', 'data': {'supportedTypes': [], 'supportedNetworks': []}}], buildDetails());
}, 'Empty basic card parameters should not throw.');

test(function() {
  new PaymentRequest([{'supportedMethods': 'not-basic-card', 'data': {'supportedTypes': 0, 'supportedNetworks': 'foo'}}], buildDetails());
}, 'Invalid basic card parameters should not throw when method name is not "basic-card".');

test(function() {
    new PaymentRequest([{'supportedMethods': 'https://android.com/pay', 'data': {'merchantName': 'Merchant Inc', 'merchantId': '123', 'allowedCardNetworks': ['AMEX', 'DISCOVER', 'MASTERCARD', 'VISA'], 'paymentMethodTokenizationParameters': {'tokenizationType': 'NETWORK_TOKEN', 'parameters': {'key': 'value'}}}}], buildDetails());
}, 'Android Pay parameters for network token without environment key should not throw.');

test(function() {
    new PaymentRequest([{'supportedMethods': 'https://bobpay.com', 'data': {'allowedCardNetworks': 0}}], buildDetails());
}, 'Invalid Android Pay parameters should not throw when method name is not "https://android.com/pay".');

test(function() {
    new PaymentRequest([{'supportedMethods': 'https://android.com/pay', 'data': {'allowedCardNetworks': 0}}], buildDetails());
}, 'Invalid Android Pay parameters should not throw even when method name is "https://android.com/pay".');

test(function() {
    new PaymentRequest([{'supportedMethods': 'foo', 'data': []}], buildDetails());
}, 'Array value for payment method specific data parameter should not throw');

promise_test(function(t) {
    return promise_rejects_dom(t, 'InvalidStateError', new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails()).abort());
}, 'abort() without show() should reject with error');

generate_tests(assert_throws_js, [
    ['PaymentRequest constructor should throw for incorrect parameter types.', TypeError, function() {
        new PaymentRequest('', '', '')
    }],
    ['PaymentRequest constructor should throw for undefined required parameters.', TypeError, function() {
        new PaymentRequest(undefined, undefined)
    }],
    ['PaymentRequest constructor should throw for null required parameter.', TypeError, function() {
        new PaymentRequest(null, null)
    }],
    ['Empty list of supported payment method identifiers should throw TypeError.', TypeError, function() {
        new PaymentRequest([], buildDetails())
    }],
    ['Empty supported payment method identifier should throw RangeError.', RangeError, function() {
        new PaymentRequest([{'supportedMethods': ''}], buildDetails())
    }],
    ['Absence of total should throw TypeError.', TypeError, function() {
        new PaymentRequest([{'supportedMethods': 'foo'}], {'displayItems': [buildItem()]})
    }],
    ['Negative total value should throw a TypeError.', TypeError, function() {
        new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails('total', {'value': '-0.01'}))
    }],
    ['Negative total value in PaymentDetailsModifier should throw a TypeError.', TypeError, function() {
        new PaymentRequest([{'supportedMethods': 'foo'}], {'total': buildItem(), 'modifiers': [{'supportedMethods': 'foo', 'total': buildItem({'value': '-0.01'})}]})
    }],
    ['Undefined supportedMethods in modifiers should throw TypeError.', TypeError, function() {
        new PaymentRequest([{'supportedMethods': 'foo'}], {'total': buildItem(), 'modifiers': [{'supportedMethods': undefined}]})
    }],
    ['Empty supportedMethods in modifiers should throw RangeError.', RangeError, function() {
        new PaymentRequest([{'supportedMethods': 'foo'}], {'total': buildItem(), 'modifiers': [{'supportedMethods': ''}]})
    }],
    ['Absence of supportedMethods in modifiers should throw TypeError.', TypeError, function() {
        new PaymentRequest([{'supportedMethods': 'foo'}], {'total': buildItem(), 'modifiers': [{'total': buildItem()}]})
    }],
    ['Empty details should throw', TypeError, function() {
        new PaymentRequest([{'supportedMethods': 'foo'}], {})
    }],
    ['Null items should throw', TypeError, function() {
        new PaymentRequest([{'supportedMethods': 'foo'}], {'total': buildItem(), 'displayItems': null});
    }],
    ['Null shipping options should throw', TypeError, function() {
        new PaymentRequest([{'supportedMethods': 'foo'}], {'total': buildItem(), 'displayItems': [buildItem()], 'shippingOptions': null});
    }],
    ['Undefined PaymentShippingType value for shppingType should throw a TypeError', TypeError, function() {
        var request = new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails(), {'requestShipping': true, 'shippingType': 'invalid'});
    }],
    ['Null for shppingType should throw a TypeError', TypeError, function() {
        var request = new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails(), {'requestShipping': true, 'shippingType': null});
    }],
    ['Array value for shppingType should throw a TypeError', TypeError, function() {
        var request = new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails(), {'requestShipping': true, 'shippingType': []});
    }],
    ['Object value for shppingType should throw a TypeError', TypeError, function() {
        var request = new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails(), {'requestShipping': true, 'shippingType': {}});
    }],
    ['Numeric value for shppingType should throw a TypeError', TypeError, function() {
        var request = new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails(), {'requestShipping': true, 'shippingType': 0});
    }],

    // Payment method specific data should be a JSON-serializable object.
    ['String value for payment method specific data parameter should throw', TypeError, function() {
        new PaymentRequest([{'supportedMethods': 'foo', 'data': 'foo'}], buildDetails(), {})
    }],
    ['Numeric value for payment method specific data parameter should throw', TypeError, function() {
        new PaymentRequest([{'supportedMethods': 'foo', 'data': 42}], buildDetails(), {})
    }],
    ['Infinite JSON value for one of the payment method specific data pieces should throw', TypeError, function() {
        var infiniteData = {'foo': {}};
        infiniteData.foo = infiniteData;
        new PaymentRequest([{'supportedMethods': 'foo', 'data': infiniteData}], buildDetails())
    }],
    ['Null for payment method specific data parameter should throw', TypeError, function() {
        new PaymentRequest([{'supportedMethods': 'foo', 'data': null}], buildDetails())
    }],
    ['Empty string for payment method specific data parameter should throw', TypeError, function() {
        new PaymentRequest([{'supportedMethods': 'foo', 'data': ''}], buildDetails())
    }],
    ['PaymentRequest constructor should throw for invalid "basic-card" parameters.', TypeError, function() {
        new PaymentRequest([{'supportedMethods': 'basic-card', 'data': {'supportedTypes': 0, 'supportedNetworks': 'foo'}}], buildDetails())
    }]
]);

var detailNames = ['total', 'displayItems.0', 'shippingOptions.0', 'modifiers.0.total', 'modifiers.0.additionalDisplayItems.0'];
for (var i in detailNames) {
    generate_tests(assert_throws_js, [
        // Invalid currency code formats.
        ['Undefined currency code in ' + detailNames[i] + ' should throw', TypeError, function() {
            new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails(detailNames[i], {'currency': undefined}), {requestShipping: true})
        }],

        // Invalid amount formats.
        ['Invalid amount "-" in ' + detailNames[i] + ' should throw', TypeError, function() {
            new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails(detailNames[i], {'value': '-'}), {requestShipping: true})
        }],
        ['Invalid amount "notdigits" in ' + detailNames[i] + ' should throw', TypeError, function() {
            new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails(detailNames[i], {'value': 'notdigits'}), {requestShipping: true})
        }],
        ['Invalid amount "ALSONOTDIGITS" in ' + detailNames[i] + ' should throw', TypeError, function() {
            new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails(detailNames[i], {'value': 'ALSONOTDIGITS'}), {requestShipping: true})
        }],
        ['Invalid amount "10." in ' + detailNames[i] + ' should throw', TypeError, function() {
            new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails(detailNames[i], {'value': '10.'}), {requestShipping: true})
        }],
        ['Invalid amount ".99" in ' + detailNames[i] + ' should throw', TypeError, function() {
            new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails(detailNames[i], {'value': '.99'}), {requestShipping: true})
        }],
        ['Invalid amount "-10." in ' + detailNames[i] + ' should throw', TypeError, function() {
            new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails(detailNames[i], {'value': '-10.'}), {requestShipping: true})
        }],
        ['Invalid amount "-.99" in ' + detailNames[i] + ' should throw', TypeError, function() {
            new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails(detailNames[i], {'value': '-.99'}), {requestShipping: true})
        }],
        ['Invalid amount "10-" in ' + detailNames[i] + ' should throw', TypeError, function() {
            new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails(detailNames[i], {'value': '10-'}), {requestShipping: true})
        }],
        ['Invalid amount "1-0" in ' + detailNames[i] + ' should throw', TypeError, function() {
            new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails(detailNames[i], {'value': '1-0'}), {requestShipping: true})
        }],
        ['Invalid amount "1.0.0" in ' + detailNames[i] + ' should throw', TypeError, function() {
            new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails(detailNames[i], {'value': '1.0.0'}), {requestShipping: true})
        }],
        ['Invalid amount "1/3" in ' + detailNames[i] + ' should throw', TypeError, function() {
            new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails(detailNames[i], {'value': '1/3'}), {requestShipping: true})
        }],
        ['Empty amount in ' + detailNames[i] + ' should throw', TypeError, function() {
            new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails(detailNames[i], {'value': ''}), {requestShipping: true})
        }],
        ['Null amount in ' + detailNames[i] + ' should throw', TypeError, function() {
            new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails(detailNames[i], {'value': null}), {requestShipping: true})
        }],
        ['Undefined amount in ' + detailNames[i] + ' should throw', TypeError, function() {
            new PaymentRequest([{'supportedMethods': 'foo'}], buildDetails(detailNames[i], {'value': undefined}), {requestShipping: true})
        }],
    ]);
}
</script>
